package com.cloudkinto.service.order.outbound.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cloudkinto.bo.storage.CellAvailableBySkuQueryBo;
import com.cloudkinto.bo.wave.*;
import com.cloudkinto.common.BeanConvert;
import com.cloudkinto.common.DynamicTablePolicy;
import com.cloudkinto.common.RabbitQueueConstant;
import com.cloudkinto.common.constant.SysConstant;
import com.cloudkinto.common.exception.BizException;
import com.cloudkinto.common.web.PageResult;
import com.cloudkinto.common.web.SingleResult;
import com.cloudkinto.dao.OrderOutboundDao;
import com.cloudkinto.dao.OutboundWaveDao;
import com.cloudkinto.dto.print.WavePickListByClassifyTableDto;
import com.cloudkinto.dto.print.WavePickListByTotalTableDto;
import com.cloudkinto.dto.print.common.DynamicTableDto;
import com.cloudkinto.dto.product.ProductPackInfoDto;
import com.cloudkinto.entity.*;
import com.cloudkinto.enumType.common.CountryEnum;
import com.cloudkinto.enumType.common.ProgressEnum;
import com.cloudkinto.enumType.operate.OperateTypeEnum;
import com.cloudkinto.enumType.order.outbound.*;
import com.cloudkinto.service.channel.LogisticsChannelService;
import com.cloudkinto.service.company.CompanyService;
import com.cloudkinto.service.file.FileService;
import com.cloudkinto.service.operate.OperateRecordService;
import com.cloudkinto.service.order.box.OrderBoxService;
import com.cloudkinto.service.order.outbound.*;
import com.cloudkinto.service.order.picking.OutboundPickingService;
import com.cloudkinto.service.product.ProductService;
import com.cloudkinto.service.progress.ProgressService;
import com.cloudkinto.service.sowWall.SowWallService;
import com.cloudkinto.service.stock.ProductStockRackService;
import com.cloudkinto.service.storage.StorageLocationService;
import com.cloudkinto.service.user.UserService;
import com.cloudkinto.utils.AsposePdfUtils;
import com.cloudkinto.utils.BarCodeUtils;
import com.cloudkinto.vo.common.ListPageStatusStaDto;
import com.cloudkinto.vo.order.outbound.*;
import com.cloudkinto.vo.storage.CellAvailableVo;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.config.ConfigureBuilder;
import com.deepoove.poi.data.*;
import com.deepoove.poi.util.PoitlIOUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * <p>
 * 出库波次 service实现类
 * </p>
 *
 * @author long
 * @since 2024-12-03
 */
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class OutboundWaveServiceImpl extends ServiceImpl<OutboundWaveDao, OutboundWaveDo> implements OutboundWaveService {

    private final LogisticsChannelService logisticsChannelService;

    private final OrderOutboundDao orderOutboundDao;

    @Resource
    @Lazy
    private OutboundService outboundService;

    private final OrderOutboundProductService outboundProductService;

    private final OperateRecordService operateRecordService;

    private final ProgressService progressService;

    private final ProductService productService;

    private final OutboundPickingService pickingService;

    private final StorageLocationService storageLocationService;

    private final ProductStockRackService productStockRackService;

    private final CompanyService companyService;

    private final SowWallService sowWallService;

    private final OrderOutboundPackageService outboundPackageService;

    private final OrderOutboundPackageMaterialService outboundPackageMaterialService;

    private final FileService fileService;

    private final UserService userService;

    private final RabbitTemplate rabbitTemplate;

    private final OrderBoxService boxService;

    @Override
    public WaveDetail detail(Long id) {
        OutboundWaveDo waveDo = baseMapper.selectById(id);
        if (waveDo == null) {
            return null;
        }
//        WaveDetail res = new WaveDetail();
//        BeanUtils.copyProperties(waveDo, res);
        WaveDetail res = BeanConvert.INSTANCE.waveDoToWaveDetail(waveDo);
        return this.handlerDetailAfter(res);
    }

    /**
     * 查询明细之后要执行的业务定制处理
     */
    private WaveDetail handlerDetailAfter(WaveDetail res) {
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(Wrappers.<OrderOutboundDo>lambdaQuery().eq(OrderOutboundDo::getWaveId, res.getId()));
        Map<Long, List<OrderOutboundProductDo>> outIdMatchProduct = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outIdMatchProduct.values().stream().flatMap(List::stream).map(OrderOutboundProductDo::getProductId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        Map<Long, List<OrderOutboundProductDo>> outIdMatchProductCopy = new HashMap<>(outIdMatchProduct);
        Collection<OrderOutboundProductDo> outboundProductDos = outIdMatchProductCopy.values().stream().flatMap(List::stream).collect(Collectors.toMap(OrderOutboundProductDo::getProductId, val -> val, (v1, v2) -> {
            v1.setForecastQty(v1.getForecastQty() + v2.getForecastQty());
            v1.setOutboundQty(v1.getOutboundQty() + v2.getOutboundQty());
            return v1;
        })).values();
        List<WaveDetail.SkuDetail> skuDetailList = JSONUtil.toList(JSONUtil.toJsonStr(outboundProductDos), WaveDetail.SkuDetail.class);
        skuDetailList.forEach(skuDetail -> {
            skuDetail.setProductName(productIdMatch.get(skuDetail.getProductId()).getProductName());
            skuDetail.setSku(productIdMatch.get(skuDetail.getProductId()).getSku());
        });
        res.setSkuDetailList(skuDetailList);
        List<NormalOutboundVo> outboundVoList = JSONUtil.toList(JSONUtil.toJsonStr(outboundDoList), NormalOutboundVo.class);
        outboundVoList.forEach(outboundVo -> {
            List<OutboundProductVo> productDtoList = new ArrayList<>();
            outIdMatchProduct.get(outboundVo.getId()).forEach(outboundProductDo -> {
                ProductDo productDo = productIdMatch.get(outboundProductDo.getProductId());
                if (productDo == null) {
                    return;
                }
                OutboundProductVo outboundProductVo = new OutboundProductVo();
                outboundProductVo.setSku(productDo.getSku());
                outboundProductVo.setQty(outboundProductDo.getForecastQty());
                outboundProductVo.setProductName(productDo.getProductName());
                productDtoList.add(outboundProductVo);
            });
            outboundVo.setProductDtoList(productDtoList);
        });
        res.setOutboundDoList(outboundVoList);
        return res;
    }

    @Override
    public SingleResult pageInit(Long userId, Long tenantId, String source) {
        HashMap<String, Object> map = new HashMap<>(8);
        map.put("logisticsChannels", logisticsChannelService.list(Wrappers.<LogisticsChannelDo>lambdaQuery().eq(LogisticsChannelDo::getTenantId, tenantId)));
        map.put("statusList", WaveStatusEnum.getWaveStatusList());
        map.put("normalStatusList", NormalOutboundStatusEnum.getList());
        map.put("companyList", companyService.getCompanyList(userId, tenantId, source));
        map.put("orderVarietyList", OutboundVarietyTypeEnum.getOrderVarietyTypeList());
        map.put("salesPlatformList", SalesPlatformEnum.getSalesPlatformList());
        map.put("lcCarrierList", logisticsChannelService.lambdaQuery().eq(LogisticsChannelDo::getTenantId, tenantId).list());
        map.put("carrierList", logisticsChannelService.lambdaQuery().list().stream().map(LogisticsChannelDo::getCarrier).collect(Collectors.toSet()));
        map.put("destinationCountryList", CountryEnum.getCountryEnumList());
        map.put("outboundOriginList", OutboundOriginEnum.getOrderOriginList());
        return SingleResult.success(map);
    }

    @Override
    public PageResult getListPage(WaveQueryBo bo) {
        Integer current = bo.getCurrent();
        Integer pageSize = bo.getPageSize();
        PageHelper.startPage(current, pageSize);
        List<OutboundWaveDo> list = baseMapper.getListPage(bo);
        PageInfo<OutboundWaveDo> pageInfo = new PageInfo<>(list);
        return new PageResult<>(
                list,
                current,
                pageSize,
                (int) pageInfo.getTotal(),
                pageInfo.getPages()
        );
    }

    @Override
    public SingleResult listPageStatusStatistics(WaveQueryBo bo) {
        Map<Integer, Integer> data = baseMapper.getListPageStatusSta(bo).stream().collect(Collectors.toMap(ListPageStatusStaDto::getStatus, ListPageStatusStaDto::getQty));
        List<Map<String, Object>> resultList = new ArrayList<>();
        for (WaveStatusEnum statusEnum : WaveStatusEnum.values()) {
            Map<String, Object> map = new HashMap<>(8);
            map.put("id", statusEnum.getValue());
            map.put("name", statusEnum.getDesc());
            map.put("sum", data.getOrDefault(statusEnum.getValue(), 0));
            resultList.add(map);
        }
        return SingleResult.success(resultList);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int cannelWave(Long id, Long userId, String username) {
        OutboundWaveDo waveDo = this.getById(id);
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        if (!Objects.equals(waveDo.getStatus(), WaveStatusEnum.zero.getValue())) {
            throw new BizException(SysConstant.Wave_Status_Cannot_Be_Cancelled);
        }
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery().eq(OrderOutboundDo::getWaveId, id)
        );
        if (CollUtil.isNotEmpty(outboundDoList)) {
            outboundProductService.lambdaUpdate()
                    .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                    .set(OrderOutboundProductDo::getOutboundQty, 0)
                    .set(OrderOutboundProductDo::getOutboundBoxQty, 0)
                    .update();
            for (OrderOutboundDo orderOutboundDo : outboundDoList) {
                operateRecordService.operateAddRecordAndValue(
                        SysConstant.OrderOutbound_Cancel_Wave,
                        "",
                        orderOutboundDo.getId(),
                        userId,
                        username,
                        OperateTypeEnum.OrderOutbound
                );
                progressService.lambdaUpdate()
                        .eq(ProgressDo::getBizNumber, orderOutboundDo.getId())
                        .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                        .notIn(ProgressDo::getBizType, "创建", "提交")
                        .set(ProgressDo::getOperateBy, null)
                        .set(ProgressDo::getOperateTime, null)
                        .update();
                //修改生成波次时间及状态
                OrderOutboundDo update = new OrderOutboundDo();
                update.setCreateWaveTime(null);
                update.setWaveId(null);
                update.setId(orderOutboundDo.getId());
                update.setStatus(NormalOutboundStatusEnum.one.getValue());
                orderOutboundDao.updateById(update);
            }
        }
        operateRecordService.remove(
                Wrappers.<OperateRecordDo>lambdaQuery()
                        .eq(OperateRecordDo::getThirdId, id)
                        .eq(OperateRecordDo::getType, OperateTypeEnum.Wave.getValue())
        );
        //删除波次
        return baseMapper.deleteById(id);
    }

    @Override
    public void assignPickers(List<Long> ids, Long pickerId) {
        if (CollUtil.isEmpty(ids)) {
            return;
        }
        this.lambdaUpdate()
                .in(OutboundWaveDo::getId, ids)
                .set(OutboundWaveDo::getPicker, pickerId)
                .update();
    }

    @Override
    public WavePickingDetail wavePickingDetail(Long id) {
        OutboundWaveDo waveDo = this.getById(id);
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.No_Data);
        }
        if (waveDo.getStatus().equals(WaveStatusEnum.two.getValue())) {
            throw new BizException(SysConstant.Wave_Status_Cannot_Be_Picked);
        }
//        WavePickingDetail wavePickingDetail = new WavePickingDetail();
//        BeanUtils.copyProperties(waveDo, wavePickingDetail);
        WavePickingDetail wavePickingDetail = BeanConvert.INSTANCE.waveDoToWavePickingDetail(waveDo);
        wavePickingDetail.setStatus(WaveStatusEnum.getDescByValue(waveDo.getStatus()));
        //所有出库单id
        List<Long> outIdList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery().eq(OrderOutboundDo::getWaveId, id)
        ).stream().map(OrderOutboundDo::getId).collect(Collectors.toList());
        //出库单产品按产品id分组
        Map<Long, List<OrderOutboundProductDo>> outboundProductByProductIdGroup = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outIdList)
                .apply("outbound_qty < forecast_qty")
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getProductId));
        if (CollUtil.isEmpty(outboundProductByProductIdGroup)) {
            return wavePickingDetail;
        }
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductByProductIdGroup.keySet())
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //待拣货的推荐库位
        List<OutboundPickingDo> noPickingDoList = pickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outboundProductByProductIdGroup.values().stream().flatMap(List::stream).map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                .eq(OutboundPickingDo::getPickFlag, Boolean.FALSE)
                .list();
        List<WavePickingDetail.PackingDetailBySku> packingList = new ArrayList<>();
        for (Map.Entry<Long, List<OrderOutboundProductDo>> entry : outboundProductByProductIdGroup.entrySet()) {
            Long productId = entry.getKey();
            List<OrderOutboundProductDo> value = entry.getValue();
            List<Long> outboundProductIdList = value.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList());
            ProductDo productDo = productIdMatch.get(productId);
            if (productDo == null) {
                continue;
            }
            WavePickingDetail.PackingDetailBySku packingDetailBySku = new WavePickingDetail.PackingDetailBySku();
            List<OutboundPickingDo> noPickingListBySku = noPickingDoList.stream()
                    .filter(item -> outboundProductIdList.contains(item.getOutboundProductId()))
                    .collect(Collectors.toList());
//            if (CollUtil.isEmpty(noPickingListBySku)) {
//                continue;
//            }
            //根据sku查库位可用数量包括推荐库位已锁定的数据
            Map<Long, CellAvailableVo> cellAvailableDtoMap = storageLocationService.getCellAvailableByProductId(
                    CellAvailableBySkuQueryBo.builder()
                            .wareId(id)
                            .productId(productId)
                            .cellIdList(
                                    noPickingListBySku.stream()
                                            .map(OutboundPickingDo::getRackId)
                                            .collect(Collectors.toSet())
                            )
                            .build()
            ).stream().collect(Collectors.toMap(CellAvailableVo::getCellId, val -> val));
            for (OutboundPickingDo pickingDo : noPickingListBySku.stream().collect(Collectors.toMap(
                    OutboundPickingDo::getRackId, val -> val, (o1, o2) -> {
                        o1.setQty(o1.getQty() + o2.getQty());
                        return o1;
                    })).values()) {
                packingDetailBySku.getCellInfoList().add(
                        WavePickingDetail.CellInfo.builder()
                                .cellId(pickingDo.getRackId())
                                .cellCode(cellAvailableDtoMap.get(pickingDo.getRackId()).getCellCode())
                                .availableQty(cellAvailableDtoMap.get(pickingDo.getRackId()).getAvailableQty())
                                .qty(pickingDo.getQty())
                                .build()
                );
            }
            //构建数据
            packingDetailBySku.setSku(productDo.getSku());
            packingDetailBySku.setProductId(productId);
            packingDetailBySku.setMainBarcode(productDo.getJanCode());
            packingDetailBySku.setProductName(productDo.getProductName());
            packingDetailBySku.setOutboundQty(value.stream().mapToInt(OrderOutboundProductDo::getOutboundQty).sum());
            packingDetailBySku.setForecastQty(value.stream().mapToInt(OrderOutboundProductDo::getForecastQty).sum());
            packingList.add(packingDetailBySku);
        }
        wavePickingDetail.setPackingList(packingList);
        return wavePickingDetail;
    }

    @Override
    public boolean wavePickingOperate(WavePickingBo bo, Long userId, String username, Long tenantId) {
        OutboundWaveDo waveDo = this.getById(bo.getWaveId());
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.No_Data);
        }
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery()
                        .eq(OrderOutboundDo::getWaveId, bo.getWaveId())
        );
        //出库单产品按sku分组
        Map<Long, List<OrderOutboundProductDo>> productIdMatchOutProductList = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getProductId));
        Map<Long, List<OutboundPickingDo>> outProductIdMatchNoPickList = pickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, productIdMatchOutProductList.values().stream().flatMap(Collection::stream).map(OrderOutboundProductDo::getId).collect(Collectors.toSet()))
                .eq(OutboundPickingDo::getPickFlag, Boolean.FALSE)
                .list().stream().collect(Collectors.groupingBy(OutboundPickingDo::getOutboundProductId));
        for (WavePickingBo.WavePicking pickingDetail : bo.getWavePickingList()) {
            String sku = pickingDetail.getSku();
            Long productId = pickingDetail.getProductId();
            //出库单产品按产品id分组
            List<Long> outboundProductIdBySkuList = productIdMatchOutProductList.get(productId).stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList());
            //查询未拣货的记录（推荐库位）
//            List<OutboundPickingDo> noPickingDoList = pickingService.lambdaQuery()
//                    .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
//                    .in(OutboundPickingDo::getOutboundProductId, outboundProductIdBySkuList)
//                    .eq(OutboundPickingDo::getPickFlag, Boolean.FALSE)
//                    .list();
            List<OutboundPickingDo> noPickingDoList = new ArrayList<>();
            for (Long outProductId : outboundProductIdBySkuList) {
                List<OutboundPickingDo> pickingDoList = outProductIdMatchNoPickList.get(outProductId);
                if (CollUtil.isNotEmpty(pickingDoList)) {
                    noPickingDoList.addAll(pickingDoList);
                }
            }
            if (CollUtil.isEmpty(noPickingDoList)) {
                continue;
            }
            Map<Long, List<OutboundPickingDo>> noPickingDoListMap = noPickingDoList.stream()
                    .collect(Collectors.groupingBy(OutboundPickingDo::getRackId));
            //实际库位
            List<WavePickingBo.WavePickingCell> pickingCellList = pickingDetail.getPickingCellList();
            Map<Long, WavePickingBo.WavePickingCell> pickingCellMap = pickingCellList.stream()
                    .collect(Collectors.toMap(WavePickingBo.WavePickingCell::getCellId, val -> val));
            //计算推荐/实际拣货总数
            Integer noPickingTotalQty = noPickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum);
            Integer pickingTotalQty = pickingCellList.stream().map(WavePickingBo.WavePickingCell::getQty).reduce(0, Integer::sum);
            //比较推荐/实际拣货总数
            if (pickingTotalQty <= noPickingTotalQty) {
                //实际拣货库位在推荐库位中
                for (Map.Entry<Long, List<OutboundPickingDo>> entry : noPickingDoListMap.entrySet()) {
                    Long rackId = entry.getKey();
                    List<OutboundPickingDo> pickingDoListByRackId = entry.getValue();
                    //计算推荐库位拣货数量
                    Integer noPickingQty = pickingDoListByRackId.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum);
                    WavePickingBo.WavePickingCell pickingCell = pickingCellMap.get(rackId);
                    if (Objects.isNull(pickingCell)) {
                        continue;
                    }
                    //实际库位拣货数量
                    Integer pickingQty = pickingCell.getQty();
                    if (pickingQty < noPickingQty) {
                        //实际拣货数量小于推荐拣货数量 eg.推荐拣货数量为3,实际拣货数量为1的情况
                        AtomicInteger needUpdate = new AtomicInteger(pickingQty);
                        //修改推荐库位的状态
                        pickingDoListByRackId.stream()
                                .sorted(Comparator.comparing(OutboundPickingDo::getId))
                                .forEach(item -> {
                                    if (needUpdate.get() == 0) {
                                        //跳过档次循环
                                        return;
                                    }
                                    if (item.getQty() <= needUpdate.get()) {
                                        pickingService.lambdaUpdate()
                                                .eq(OutboundPickingDo::getId, item.getId())
                                                .set(OutboundPickingDo::getPickFlag, Boolean.TRUE)
                                                .update();
                                        noPickingDoList.forEach(noPickingDo -> {
                                            if (noPickingDo.getId().equals(item.getId())) {
                                                noPickingDo.setPickFlag(Boolean.TRUE);
                                            }
                                        });
                                        needUpdate.set(needUpdate.get() - item.getQty());
                                    } else {
                                        pickingService.lambdaUpdate()
                                                .eq(OutboundPickingDo::getId, item.getId())
                                                .setSql("qty = qty -" + needUpdate.get())
                                                .update();
                                        //修改待拣货列表数据
                                        noPickingDoList.forEach(noPickingDo -> {
                                            if (noPickingDo.getId().equals(item.getId())) {
                                                noPickingDo.setQty(noPickingDo.getQty() - needUpdate.get());
                                            }
                                        });
//                                        OutboundPickingDo pickingDo = new OutboundPickingDo();
//                                        BeanUtils.copyProperties(item, pickingDo);
                                        OutboundPickingDo pickingDo = BeanConvert.INSTANCE.copyPickingDo(item);
                                        pickingDo.setId(null);
                                        pickingDo.setOrderType(OutboundOrderKind.outbound.getValue());
                                        pickingDo.setQty(needUpdate.get());
                                        pickingDo.setPickFlag(Boolean.TRUE);
                                        pickingDo.setCreateTime(DateUtil.date());
                                        pickingService.save(pickingDo);
                                        noPickingDoList.add(pickingDo);
                                        needUpdate.set(0);
                                    }
                                });
                    } else {
                        //实际库位拣货数量等于推荐拣货数量的部分
                        pickingService.lambdaUpdate()
                                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                                .in(OutboundPickingDo::getOutboundProductId, outboundProductIdBySkuList)
                                .eq(OutboundPickingDo::getRackId, rackId)
                                .set(OutboundPickingDo::getPickFlag, Boolean.TRUE)
                                .update();
                        noPickingDoList.forEach(noPickingDo -> {
                            if (outboundProductIdBySkuList.contains(noPickingDo.getOutboundProductId())) {
                                if (noPickingDo.getRackId().equals(rackId)) {
                                    noPickingDo.setPickFlag(Boolean.TRUE);
                                }
                            }
                        });
                        if (pickingQty > noPickingQty) {
                            //实际库位拣货数量大于推荐拣货数量的部分
                            AtomicInteger needLockOrUnlock = new AtomicInteger(pickingQty - noPickingQty);
                            List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                                    tenantId,
                                    waveDo.getWhId(),
                                    productId,
                                    needLockOrUnlock.get(),
                                    pickingCell.getCellId()
                            );
                            //锁定库存&解锁库存 解锁库存,优先解锁非选择的库位，再根据推荐库位解锁倒序解锁
                            this.lockAndUnlockStockAndSavePicking(
                                    productStockRackDoList,
                                    needLockOrUnlock,
                                    sku,
                                    noPickingDoList.stream()
                                            .filter(noPickingDo -> Boolean.FALSE.equals(noPickingDo.getPickFlag())
                                                                   && !pickingCellMap.containsKey(noPickingDo.getRackId()))
                                            .collect(Collectors.toList()),
                                    noPickingDoList
                            );
                            this.lockAndUnlockStockAndSavePicking(
                                    productStockRackDoList,
                                    needLockOrUnlock,
                                    sku,
                                    noPickingDoList.stream()
                                            .filter(noPickingDo -> Boolean.FALSE.equals(noPickingDo.getPickFlag())
                                                                   && pickingCellMap.containsKey(noPickingDo.getRackId()))
                                            .collect(Collectors.toList()),
                                    noPickingDoList
                            );
                        }
                    }
                }
                //拣货库位不在推荐库位范围内
                for (Map.Entry<Long, WavePickingBo.WavePickingCell> pickingCellEntry : pickingCellMap.entrySet()) {
                    Long cellId = pickingCellEntry.getKey();
                    Integer qty = pickingCellEntry.getValue().getQty();
                    if (noPickingDoListMap.containsKey(cellId)) {
                        continue;
                    }
                    //锁定库存
                    AtomicInteger needLockOrUnlock = new AtomicInteger(qty);
                    List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                            tenantId,
                            waveDo.getWhId(),
                            productId,
                            needLockOrUnlock.get(),
                            cellId
                    );
                    //锁定库存&解锁库存 解锁库存,优先解锁非选择的库位，再根据推荐库位解锁倒序解锁
                    this.lockAndUnlockStockAndSavePicking(
                            productStockRackDoList,
                            needLockOrUnlock,
                            sku,
                            noPickingDoList.stream()
                                    .filter(noPickingDo -> Boolean.FALSE.equals(noPickingDo.getPickFlag())
                                                           && !pickingCellMap.containsKey(noPickingDo.getRackId()))
                                    .collect(Collectors.toList()),
                            noPickingDoList
                    );
                    this.lockAndUnlockStockAndSavePicking(
                            productStockRackDoList,
                            needLockOrUnlock,
                            sku,
                            noPickingDoList.stream()
                                    .filter(noPickingDo -> Boolean.FALSE.equals(noPickingDo.getPickFlag())
                                                           && pickingCellMap.containsKey(noPickingDo.getRackId()))
                                    .collect(Collectors.toList()),
                            noPickingDoList
                    );
                }
            } else {
                //超拣
                throw new BizException(SysConstant.PickingQty_Exceeds_ShouldPickingQty, sku);
            }
            AtomicInteger pickingNum = new AtomicInteger(pickingTotalQty);
            for (OrderOutboundProductDo outboundProductDo : productIdMatchOutProductList.get(productId)) {
                if (pickingNum.get() <= 0) {
                    break;
                }
                Integer outboundQty = outboundProductDo.getOutboundQty();
                Integer forecastQty = outboundProductDo.getForecastQty();
                if (outboundQty < forecastQty) {
                    int qty = forecastQty - outboundQty;
                    if (qty <= pickingNum.get()) {
                        //修改出库单产品实际拣货数量
                        outboundProductService.lambdaUpdate()
                                .eq(OrderOutboundProductDo::getId, outboundProductDo.getId())
                                .setSql("outbound_qty = outbound_qty + " + qty)
                                .update();
                        pickingNum.set(pickingNum.get() - qty);
                    } else {
                        //修改出库单产品实际拣货数量
                        outboundProductService.lambdaUpdate()
                                .eq(OrderOutboundProductDo::getId, outboundProductDo.getId())
                                .setSql("outbound_qty = outbound_qty + " + pickingNum.get())
                                .update();
                        pickingNum.set(0);
                    }
                }
            }
        }
        List<WavePickingBo.WavePickingCell> pickingCellList = bo.getWavePickingList().stream()
                .map(WavePickingBo.WavePicking::getPickingCellList)
                .flatMap(List::stream)
                .collect(Collectors.toList());
        //计算本次拣货件数
        Integer thisTimePickingQty = pickingCellList.stream()
                .map(WavePickingBo.WavePickingCell::getQty)
                .reduce(0, Integer::sum);
        Integer shouldPickingQty = productIdMatchOutProductList.values().stream()
                .flatMap(List::stream)
                .map(item -> item.getOutboundQty() < item.getForecastQty() ? item.getForecastQty() - item.getOutboundQty() : 0)
                .reduce(0, Integer::sum);
        //修改拣货状态为拣货中,修改拣货数量
        this.lambdaUpdate()
                .eq(OutboundWaveDo::getId, bo.getWaveId())
                .set(OutboundWaveDo::getStatus, WaveStatusEnum.one.getValue())
                .set(OutboundWaveDo::getPickingQty, waveDo.getPickingQty() + thisTimePickingQty)
                .update();
        boolean pickingFinish = Objects.equals(thisTimePickingQty, shouldPickingQty);
        if (pickingFinish) {
            wavePickingFinishHandler(bo.getWaveId(), userId, username, SysConstant.OrderOutbound_Pick_Finish, outboundDoList);
            return true;
        }
        if (thisTimePickingQty > shouldPickingQty) {
            rabbitTemplate.convertAndSend(
                    RabbitQueueConstant.Wave_Force_Finish,
                    JSONUtil.toJsonStr(WaveForceFinishBo.builder().waveId(bo.getWaveId()).userId(userId).username(username).build())
            );
            return true;
        }
        return false;
    }

    @Override
    public void wavePickingFinishHandler(Long waveId, Long userId, String username, String operateKey, List<OrderOutboundDo> outboundDoList) {
        //拣货完成
        this.lambdaUpdate()
                .eq(OutboundWaveDo::getId, waveId)
                .set(OutboundWaveDo::getStatus, WaveStatusEnum.two.getValue())
                .set(OutboundWaveDo::getPickTime, DateUtil.date())
                .update();
        operateRecordService.operateAddRecordAndValue(
                operateKey,
                "",
                waveId,
                userId,
                username,
                OperateTypeEnum.Wave
        );
        if (CollUtil.isEmpty(outboundDoList)) {
            return;
        }
        for (OrderOutboundDo outboundDo : outboundDoList) {
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.OrderOutbound_Pick_Finish,
                    "",
                    outboundDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
            //进度
            progressService.lambdaUpdate()
                    .eq(ProgressDo::getBizNumber, outboundDo.getId())
                    .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                    .eq(ProgressDo::getBizType, "拣货")
                    .set(ProgressDo::getBizType, "拣货完成")
                    .set(ProgressDo::getOperateBy, username)
                    .set(ProgressDo::getOperateTime, DateUtil.date())
                    .update();
            //拣货时间&状态
            OrderOutboundDo update = new OrderOutboundDo();
            update.setId(outboundDo.getId());
            update.setPickingTime(DateUtil.date());
            update.setStatus(NormalOutboundStatusEnum.three.getValue());
            orderOutboundDao.updateById(update);
        }
    }


    @Override
    public void getSecondarySortingPreJudge(String waveNo) {
        OutboundWaveDo waveDo = this.lambdaQuery()
                .eq(OutboundWaveDo::getWaveNo, waveNo)
                .one();
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        if (!WaveStatusEnum.two.getValue().equals(waveDo.getStatus())) {
            throw new BizException(SysConstant.Wave_No_Complete_Picking);
        }
        if (waveDo.getSecondarySorting()) {
            throw new BizException(SysConstant.Wave_Complete_Secondary_Sorting);
        }
        if (waveDo.getRecheck()) {
            throw new BizException(SysConstant.Wave_Complete_Recheck);
        }
        if (waveDo.getOutbound()) {
            throw new BizException(SysConstant.Wave_Complete_Outbound);
        }
    }

    @Override
    public WaveSecondarySortingDetail getSecondarySortingDetail(SecondarySortingQueryBo bo, Long whId) {
        OutboundWaveDo waveDo = this.lambdaQuery()
                .eq(OutboundWaveDo::getWaveNo, bo.getWaveNo())
                .one();
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery().eq(OrderOutboundDo::getWaveId, waveDo.getId()));
        Map<Long, List<OrderOutboundPackageDo>> outboundIdMatchPackageList = outboundPackageService.lambdaQuery()
                .in(OrderOutboundPackageDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageDo::getOutboundId));
        Map<Long, List<OrderOutboundPackageMaterialDo>> packageIdMatchMaterialList = outboundPackageMaterialService.lambdaQuery()
                .in(OrderOutboundPackageMaterialDo::getPackageId, outboundIdMatchPackageList.values().stream().flatMap(List::stream).map(OrderOutboundPackageDo::getId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageMaterialDo::getPackageId));
        //已出库订单
        List<OrderOutboundDo> orderListByFinishOutbound = outboundDoList.stream()
                .filter(item -> NormalOutboundStatusEnum.five.getValue().equals(item.getStatus()))
                .collect(Collectors.toList());
        Map<Long, List<OrderOutboundProductDo>> productListByOutboundId = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, productListByOutboundId.values().stream().flatMap(List::stream).map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        WaveSecondarySortingDetail detail = new WaveSecondarySortingDetail();
        detail.setWaveNo(bo.getWaveNo());
        int orderSize = outboundDoList.size();
        detail.setOrderTotal(orderSize);
        //已分拣订单
        for (Map.Entry<Long, List<OrderOutboundProductDo>> orderEntry : productListByOutboundId.entrySet()) {
            if (orderEntry.getValue().stream().allMatch(item -> item.getSecondarySortingQty().equals(item.getOutboundQty())) || orderListByFinishOutbound.stream().anyMatch(item -> item.getId().equals(orderEntry.getKey()))) {
                detail.setSortedOrder(detail.getSortedOrder() + 1);
            }
        }
        if (detail.getOrderTotal() == detail.getSortedOrder() && bo.isWhetherCheck()) {
            throw new BizException(SysConstant.Wave_Complete_Secondary_Sorting);
        }
        detail.setProductTotal(productListByOutboundId.values().stream().flatMap(List::stream).mapToInt(OrderOutboundProductDo::getOutboundQty).sum());
        detail.setSortedProduct(productListByOutboundId.values().stream().flatMap(List::stream).mapToInt(OrderOutboundProductDo::getSecondarySortingQty).sum());
        if (StrUtil.isNotBlank(bo.getSowWallCode())) {
            SowWallDo sowWallDo = sowWallService.lambdaQuery()
                    .eq(SowWallDo::getSowCode, bo.getSowWallCode())
                    .one();
            detail.setGridRow(sowWallDo.getTierNum());
            detail.setGridCol(sowWallDo.getColumnNum());
        } else {
            SowWallDo wallDo = sowWallService.lambdaQuery()
                    .eq(SowWallDo::getInUseWaveNo, bo.getWaveNo())
                    .one();
            if (Objects.nonNull(wallDo)) {
                throw new BizException(SysConstant.Sys_SowWall_In_Use);
            }
        }
        if (Objects.nonNull(bo.getCustomRow())) {
            detail.setGridCol(orderSize / bo.getCustomRow() + (orderSize % bo.getCustomRow() != 0 ? 1 : 0));
            detail.setGridRow(orderSize / detail.getGridCol() + (orderSize % detail.getGridCol() != 0 ? 1 : 0));
            if (detail.getGridCol() > 10) {
                detail.setGridCol(10);
                detail.setGridRow(orderSize / detail.getGridCol() + (orderSize % detail.getGridCol() != 0 ? 1 : 0));
            }
        }
        if (Objects.nonNull(bo.getCustomColumn())) {
            detail.setGridRow(
                    orderSize / bo.getCustomColumn()
                    + (orderSize % bo.getCustomColumn() != 0 ? 1 : 0)
            );
            detail.setGridCol(bo.getCustomColumn());
        }
        List<WaveSecondarySortingDetail.GridItem> gridItemList = new ArrayList<>();
        for (int i = 0; i < outboundDoList.size(); i++) {
            OrderOutboundDo outboundDo = outboundDoList.get(i);
            WaveSecondarySortingDetail.GridItem gridItem = this.buildSecondarySortingGridItem(
                    outboundDo,
                    i + 1,
                    whId,
                    productIdMatch,
                    productListByOutboundId,
                    outboundIdMatchPackageList.get(outboundDo.getId()),
                    packageIdMatchMaterialList
            );
            gridItemList.add(gridItem);
        }
        detail.setGridItemList(gridItemList);
        return detail;
    }

    @Override
    public SecondarySortingOperateVo secondarySortingOperate(WaveSecondarySortingBo bo, Long userId, String username, Long whId) {
        OutboundWaveDo waveDo = this.lambdaQuery()
                .eq(OutboundWaveDo::getWaveNo, bo.getWaveNo())
                .one();
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        if (waveDo.getSecondarySorting()) {
            throw new BizException(SysConstant.Wave_Complete_Secondary_Sorting);
        }
        SecondarySortingOperateVo operateVo = new SecondarySortingOperateVo();
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery().eq(OrderOutboundDo::getWaveId, waveDo.getId()));
        //已出库订单
        List<Long> orderListByFinishOutbound = outboundDoList.stream()
                .filter(item -> NormalOutboundStatusEnum.five.getValue().equals(item.getStatus()))
                .collect(Collectors.toList()).stream().map(OrderOutboundDo::getId).collect(Collectors.toList());
        Map<Long, List<OrderOutboundProductDo>> productListByOutboundId = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        List<Long> skuIdListByOutboundId = productService.lambdaQuery()
                .eq(ProductDo::getSku, bo.getScan())
                .or()
                .eq(ProductDo::getJanCode, bo.getScan())
                .list().stream().map(ProductDo::getId).collect(Collectors.toList());
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, productListByOutboundId.values().stream().flatMap(List::stream).map(OrderOutboundProductDo::getProductId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        Map<Long, String> filePath = fileService.getFilePath(productIdMatch.values().stream().map(ProductDo::getPicId)
                .filter(Objects::nonNull).collect(Collectors.toList()));
        boolean scanResult = false;
        for (int i = 0; i < outboundDoList.size(); i++) {
            if (NormalOutboundStatusEnum.five.getValue().equals(outboundDoList.get(i).getStatus())) {
                continue;
            }
            List<OrderOutboundProductDo> outboundProductDoList = productListByOutboundId.get(outboundDoList.get(i).getId());
            for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                if (skuIdListByOutboundId.contains(outboundProductDo.getProductId())
                    && outboundProductDo.getSecondarySortingQty() < outboundProductDo.getOutboundQty()) {
                    ProductDo productDo = productIdMatch.get(outboundProductDo.getProductId());
                    scanResult = true;
                    outboundProductDo.setSecondarySortingQty(outboundProductDo.getSecondarySortingQty() + 1);
                    outboundProductService.updateById(outboundProductDo);
                    if (outboundProductDoList.stream().mapToInt(OrderOutboundProductDo::getSecondarySortingQty).sum() == 1) {
                        operateVo.setSecondarySortingStatus(0);
                    }
                    if (outboundProductDoList.stream().allMatch(item -> item.getSecondarySortingQty().equals(item.getOutboundQty()))) {
                        operateVo.setSecondarySortingStatus(1);
                    }
                    if (outboundProductDoList.stream().mapToInt(OrderOutboundProductDo::getOutboundQty).sum() == 1) {
                        operateVo.setSecondarySortingStatus(2);
                    }
                    operateVo.setOutboundId(outboundProductDo.getOutboundId());
                    operateVo.setOutboundNo(outboundDoList.get(i).getOutboundNo());
                    operateVo.setPicUrl(filePath.get(productDo.getPicId()));
                    operateVo.setSku(productDo.getSku());
                    operateVo.setProductName(productDo.getProductName());
                    operateVo.setGridNo(i + 1);
                    operateVo.setSecondarySortingQty(outboundProductDo.getSecondarySortingQty());
                    operateVo.setOutboundQty(outboundProductDo.getOutboundQty());
                    //单订单分拣完成查询出库单二次分拣网格信息
                    if (operateVo.getSecondarySortingStatus().equals(1) || operateVo.getSecondarySortingStatus().equals(2)) {
                        List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                                .eq(OrderOutboundPackageDo::getOutboundId, operateVo.getOutboundId())
                                .list();
                        Map<Long, List<OrderOutboundPackageMaterialDo>> packageIdMatchMaterialList = outboundPackageMaterialService.lambdaQuery()
                                .in(OrderOutboundPackageMaterialDo::getPackageId, packageDoList.stream().map(OrderOutboundPackageDo::getId).collect(Collectors.toSet()))
                                .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageMaterialDo::getPackageId));
                        WaveSecondarySortingDetail.GridItem gridItem = this.buildSecondarySortingGridItem(
                                outboundDoList.get(i),
                                i + 1,
                                whId,
                                productIdMatch,
                                productListByOutboundId,
                                packageDoList,
                                packageIdMatchMaterialList
                        );
                        operateVo.setGridItem(gridItem);
                    }
                    break;
                }
            }
            if (scanResult) {
                break;
            }
        }
        if (!scanResult) {
            throw new BizException(SysConstant.Secondary_Sorting_Product_Error);
        }
        //判断是否分拣完成
        Integer waitPickingNum = productListByOutboundId.values()
                .stream().flatMap(List::stream)
                .filter(orderOutboundProductDo -> CollUtil.isEmpty(orderListByFinishOutbound) || !orderListByFinishOutbound.contains(orderOutboundProductDo.getOutboundId()))
                .map(item -> item.getOutboundQty() - item.getSecondarySortingQty())
                .reduce(0, Integer::sum);
        if (waitPickingNum == 0) {
            operateVo.setFinish(true);
            this.lambdaUpdate()
                    .eq(OutboundWaveDo::getId, waveDo.getId())
                    .set(OutboundWaveDo::getSecondarySorting, Boolean.TRUE)
                    .set(OutboundWaveDo::getSecondarySortingTime, DateUtil.date())
                    .update();
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.Wave_SECONDARY_SORTING_Finish,
                    "",
                    waveDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.Wave
            );
            sowWallService.lambdaUpdate()
                    .eq(SowWallDo::getInUseWaveNo, waveDo.getWaveNo())
                    .set(SowWallDo::getInUseWaveNo, null)
                    .set(SowWallDo::getInUse, Boolean.FALSE)
                    .update();
        }
        return operateVo;
    }

    /**
     * 构建二次分拣网格信息
     *
     * @param outboundDo                 出库单
     * @param gridNo                     格子号
     * @param whId                       仓库id
     * @param productIdMatch             产品匹配详细产品数据
     * @param productListByOutboundId    出库单id匹配产品
     * @param packageDoList              当前出库单包裹列表
     * @param packageIdMatchMaterialList 包裹id匹配包裹包材
     * @return 返回二次分拣网格详细数据
     */
    private WaveSecondarySortingDetail.GridItem buildSecondarySortingGridItem(
            OrderOutboundDo outboundDo,
            Integer gridNo,
            Long whId,
            Map<Long, ProductDo> productIdMatch,
            Map<Long, List<OrderOutboundProductDo>> productListByOutboundId,
            List<OrderOutboundPackageDo> packageDoList,
            Map<Long, List<OrderOutboundPackageMaterialDo>> packageIdMatchMaterialList) {
        WaveSecondarySortingDetail.GridItem gridItem = new WaveSecondarySortingDetail.GridItem();
        gridItem.setGridNo(gridNo);
        gridItem.setOutboundId(outboundDo.getId());
        gridItem.setOutboundNo(outboundDo.getOutboundNo());
        gridItem.setOutboundFlag(NormalOutboundStatusEnum.five.getValue().equals(outboundDo.getStatus()));
        List<OrderOutboundProductDo> productDoList = productListByOutboundId.get(outboundDo.getId());
        gridItem.setSecondarySortingQty(productDoList.stream().mapToInt(OrderOutboundProductDo::getSecondarySortingQty).sum());
        gridItem.setOutboundQty(productDoList.stream().mapToInt(OrderOutboundProductDo::getOutboundQty).sum());
        List<WaveSecondarySortingDetail.GridItemDetail> gridItemDetails = JSONUtil.toList(
                JSONUtil.toJsonStr(productDoList), WaveSecondarySortingDetail.GridItemDetail.class);
        for (WaveSecondarySortingDetail.GridItemDetail gridItemDetail : gridItemDetails) {
            ProductDo productDo = productIdMatch.get(gridItemDetail.getProductId());
            if (productDo == null) {
                continue;
            }
            gridItemDetail.setSku(productDo.getSku());
            gridItemDetail.setMainBarcode(productDo.getJanCode());
            gridItemDetail.setProductName(productDo.getProductName());
            if (productDo.getNeedPack()) {
                gridItemDetail.setNeedPack(true);
                List<ProductPackInfoDto> packInfoDtoList = JSONUtil.toList(JSONUtil.toJsonStr(productDo.getPackInfo()), ProductPackInfoDto.class);
                packInfoDtoList.stream().filter(item -> item.getWhId().equals(whId)).findFirst().ifPresent(item -> {
                    gridItemDetail.setRecommendPackName(item.getPackName());
                    gridItemDetail.setRecommendPackCode(item.getPackCode());
                });
            }
        }
        gridItem.setSkuList(gridItemDetails);
        // 包裹信息
//        List<Long> labelFileIdList = packageDoList.stream().map(OrderOutboundPackageDo::getLabelFileId).filter(Objects::nonNull).collect(Collectors.toList());
//        Map<Long, FileVo> fileIdMatchFileVo = new HashMap<>();
//        if (CollUtil.isNotEmpty(labelFileIdList)) {
//            fileIdMatchFileVo = fileService.getFileVoByIds(labelFileIdList.stream().map(String::valueOf).collect(Collectors.joining(","))).stream().collect(Collectors.toMap(FileVo::getId, val -> val));
//        }
        for (OrderOutboundPackageDo packageDo : packageDoList) {
//            OutboundPackageDetail packageDetail = new OutboundPackageDetail();
//            BeanUtils.copyProperties(packageDo, packageDetail);
            OutboundPackageDetail packageDetail = BeanConvert.INSTANCE.packageDoToPackageDetail(packageDo);
            //包裹包材信息
            List<OrderOutboundPackageMaterialDo> packageMaterialDoList = packageIdMatchMaterialList.get(packageDo.getId());
            if (CollUtil.isNotEmpty(packageMaterialDoList)) {
                packageDetail.setPackInfoVoList(JSONUtil.toList(JSONUtil.toJsonStr(packageMaterialDoList), PackInfoVo.class));
            }
//            if (packageDo.getLabelFileId() != null) {
//                packageDetail.setFileVo(fileIdMatchFileVo.getOrDefault(packageDo.getLabelFileId(), null));
//            }
            gridItem.getOutboundPackageList().add(packageDetail);
        }
        return gridItem;
    }

    @Override
    public void secondarySortingClearRedo(WaveSecondarySortingClearBo bo, Long userId, String username) {
        OutboundWaveDo waveDo = this.lambdaQuery()
                .eq(OutboundWaveDo::getWaveNo, bo.getWaveNo())
                .one();
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        if (waveDo.getSecondarySorting()) {
            throw new BizException(SysConstant.Wave_Complete_Secondary_Sorting);
        }
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(Wrappers.<OrderOutboundDo>lambdaQuery()
                .eq(OrderOutboundDo::getWaveId, waveDo.getId()));
        outboundProductService.lambdaUpdate()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .set(OrderOutboundProductDo::getSecondarySortingQty, 0)
                .update();
        if (StrUtil.isNotBlank(bo.getSowWallNo())) {
            sowWallService.lambdaUpdate()
                    .eq(SowWallDo::getSowCode, bo.getSowWallNo())
                    .set(SowWallDo::getInUseWaveNo, null)
                    .set(SowWallDo::getInUse, Boolean.FALSE)
                    .update();
        }
        operateRecordService.operateAddRecordAndValue(
                SysConstant.WAVE_SECONDARY_SORTING_CLEAR_REDO,
                "",
                waveDo.getId(),
                userId,
                username,
                OperateTypeEnum.Wave
        );
    }

    @Override
    public void secondarySortingForceFinish(WaveSecondarySortingForceFinishBo bo, Long userId, String username) {
        OutboundWaveDo waveDo = this.lambdaQuery()
                .eq(OutboundWaveDo::getWaveNo, bo.getWaveNo())
                .one();
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        if (waveDo.getSecondarySorting()) {
            throw new BizException(SysConstant.Wave_Complete_Secondary_Sorting);
        }
        this.lambdaUpdate()
                .eq(OutboundWaveDo::getWaveNo, bo.getWaveNo())
                .set(OutboundWaveDo::getSecondarySorting, Boolean.TRUE)
                .set(OutboundWaveDo::getSecondarySortingTime, DateUtil.date())
                .update();
        sowWallService.lambdaUpdate()
                .eq(SowWallDo::getInUseWaveNo, bo.getWaveNo())
                .set(SowWallDo::getInUseWaveNo, null)
                .set(SowWallDo::getInUse, Boolean.FALSE)
                .update();
        operateRecordService.operateAddRecordAndValue(
                SysConstant.WAVE_SECONDARY_SORTING_FORCE_FINISH,
                "",
                waveDo.getId(),
                userId,
                username,
                OperateTypeEnum.Wave
        );
    }

    @Override
    public WaveRecheckDetail getRecheckDetail(String scan, Boolean whetherCheck, Long whId) {
        if (StrUtil.isBlank(scan)) {
            throw new BizException(SysConstant.Scan_Empty);
        }
        OrderOutboundDo outboundDo = orderOutboundDao.selectOne(Wrappers.<OrderOutboundDo>lambdaQuery()
                .eq(OrderOutboundDo::getOutboundNo, scan)
        );
        if (Objects.isNull(outboundDo)) {
/*
            outboundDo = orderOutboundDao.selectOne(Wrappers.<OrderOutboundDo>lambdaQuery()
//                    .eq(OrderOutboundDo::getTrackingNumber, scan)
                            .apply("find_in_set({0},tracking_number)", scan)
                            .orderByDesc(OrderOutboundDo::getCreateTime)
                            .last("limit 1")
            );
*/
            //根据物流跟踪号查出库单
            OrderOutboundPackageDo outboundPackageDo = outboundPackageService.lambdaQuery()
                    .select(OrderOutboundPackageDo::getOutboundId)
                    .eq(OrderOutboundPackageDo::getTrackingNumber, scan)
                    .orderByDesc(OrderOutboundPackageDo::getId)
                    .last("limit 1")
                    .one();
            if (outboundPackageDo != null) {
                outboundDo = orderOutboundDao.selectById(outboundPackageDo.getOutboundId());
            }
        }
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.Scan_OutboundNo_Error);
        }
        //判断是否需要校验
        if (whetherCheck) {
            checkRecheckStatus(outboundDo);
        }
        Integer outboundMode = outboundDo.getOutboundMode();
        boolean boxOutboundFlag = false;
        if (Objects.equals(outboundMode, 2)) {
            boxOutboundFlag = true;
        }

        WaveRecheckDetail detail = new WaveRecheckDetail();
        detail.setId(outboundDo.getId());
        detail.setOutboundNo(outboundDo.getOutboundNo());
        detail.setBoxOutboundFlag(boxOutboundFlag);
        detail.setTrackingNo(outboundDo.getTrackingNumber());
        CompanyDo companyDo = companyService.getById(outboundDo.getCompanyId());
        if (companyDo != null) {
            detail.setCompanyCode(companyDo.getCompanyCode());
            detail.setCompanyName(companyDo.getName());
        }
        //查询包裹信息
        List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                .eq(OrderOutboundPackageDo::getOutboundId, outboundDo.getId())
                .list();
        Map<Long, List<OrderOutboundPackageMaterialDo>> packageMaterialListMatch = outboundPackageMaterialService.lambdaQuery()
                .in(OrderOutboundPackageMaterialDo::getPackageId, packageDoList.stream().map(OrderOutboundPackageDo::getId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageMaterialDo::getPackageId));
        List<OutboundPackageDetail> outboundPackageList = new ArrayList<>();
        for (OrderOutboundPackageDo packageDo : packageDoList) {
//            OutboundPackageDetail packageDetail = new OutboundPackageDetail();
//            BeanUtils.copyProperties(packageDo, packageDetail);
            OutboundPackageDetail packageDetail = BeanConvert.INSTANCE.packageDoToPackageDetail(packageDo);
            //包裹包材
            List<OrderOutboundPackageMaterialDo> packageMaterialDoList = packageMaterialListMatch.get(packageDo.getId());
            if (CollUtil.isNotEmpty(packageMaterialDoList)) {
                packageDetail.setPackInfoVoList(JSONUtil.toList(JSONUtil.toJsonStr(packageMaterialDoList), PackInfoVo.class));
            }
            outboundPackageList.add(packageDetail);
        }
        detail.setOutboundPackageList(outboundPackageList);
        //查询出库产品信息
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                .list();
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //出库产品
        if (!boxOutboundFlag) {
            detail.setScannedProductQty(outboundProductDoList.stream().map(OrderOutboundProductDo::getRecheckQty).reduce(0, Integer::sum));
            detail.setProductTotalQty(outboundProductDoList.stream().map(OrderOutboundProductDo::getOutboundQty).reduce(0, Integer::sum));
            Map<Long, String> filePath = fileService.getFilePath(productIdMatch.values().stream().map(ProductDo::getPicId)
                    .filter(Objects::nonNull).collect(Collectors.toList()));
            List<WaveRecheckDetail.RecheckSkuItem> recheckSkuItemList = JSONUtil.toList(
                    JSONUtil.toJsonStr(outboundProductDoList), WaveRecheckDetail.RecheckSkuItem.class);
            for (WaveRecheckDetail.RecheckSkuItem recheckSkuItem : recheckSkuItemList) {
                ProductDo productDo = productIdMatch.get(recheckSkuItem.getProductId());
                recheckSkuItem.setPicUrl(filePath.get(productDo.getPicId()));
                recheckSkuItem.setSku(productDo.getSku());
                recheckSkuItem.setJanCode(productDo.getJanCode());
                recheckSkuItem.setProductName(productDo.getProductName());
                if (productDo.getNeedPack()) {
                    recheckSkuItem.setNeedPack(true);
                    List<ProductPackInfoDto> packInfoDtoList = JSONUtil.toList(JSONUtil.toJsonStr(productDo.getPackInfo()), ProductPackInfoDto.class);
                    packInfoDtoList.stream().filter(item -> item.getWhId().equals(whId)).findFirst().ifPresent(item -> {
                        recheckSkuItem.setRecommendPackName(item.getPackName());
                        recheckSkuItem.setRecommendPackCode(item.getPackCode());
                    });
                }
            }
            detail.setRecheckSkuList(recheckSkuItemList.stream().sorted(Comparator.comparing(item -> Objects.equals(item.getRecheckQty(), item.getOutboundQty()) ? 1 : 0)).collect(Collectors.toList()));
        } else {
            //出库箱
            Map<String, List<OrderOutboundProductDo>> boxTypeNoMatchProducts = outboundProductDoList.stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getBoxTypeNumber));
            Map<String, OrderBoxDo> boxTypeNoMatchInfo = boxService.lambdaQuery()
                    .in(OrderBoxDo::getBoxTypeNumber, boxTypeNoMatchProducts.keySet())
                    .list().stream().collect(Collectors.toMap(OrderBoxDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1));
            List<WaveRecheckDetail.RecheckBoxItem> recheckBoxItemList = new ArrayList<>();
            for (Map.Entry<String, List<OrderOutboundProductDo>> entry : boxTypeNoMatchProducts.entrySet()) {
                List<OrderOutboundProductDo> value = entry.getValue();
                WaveRecheckDetail.RecheckBoxItem recheckBoxItem = new WaveRecheckDetail.RecheckBoxItem();
                recheckBoxItem.setBoxTypeNumber(entry.getKey());
                recheckBoxItem.setItfCode(boxTypeNoMatchInfo.get(entry.getKey()).getCustomizeBarcode());
                recheckBoxItem.setSkuInfo(value.stream().map(item -> productIdMatch.get(item.getProductId()).getSku() + "*" + item.getOutboundQty()).collect(Collectors.joining(",")));
                recheckBoxItem.setRecheckQty(value.get(0).getRecheckQty());
                recheckBoxItem.setOutboundQty(value.get(0).getOutboundBoxQty());
                recheckBoxItemList.add(recheckBoxItem);
            }
            detail.setScannedBoxQty(recheckBoxItemList.stream().map(WaveRecheckDetail.RecheckBoxItem::getRecheckQty).reduce(0, Integer::sum));
            detail.setBoxTotalQty(recheckBoxItemList.stream().map(WaveRecheckDetail.RecheckBoxItem::getOutboundQty).reduce(0, Integer::sum));
            detail.setRecheckBoxList(recheckBoxItemList.stream().sorted(Comparator.comparing(item -> Objects.equals(item.getRecheckQty(), item.getOutboundQty()) ? 1 : 0)).collect(Collectors.toList()));
        }
        return detail;
    }

    private static void checkRecheckStatus(OrderOutboundDo outboundDo) {
        //判断出库单状态
        if (OutboundTypeEnum.one.getValue().equals(outboundDo.getOrderType()) && !NormalOutboundStatusEnum.three.getValue().equals(outboundDo.getStatus())) {
            throw new BizException(SysConstant.Normal_Outbound_Non_Recheck_Status);
        }
        if (OutboundTypeEnum.two.getValue().equals(outboundDo.getOrderType()) && !Objects.equals(TransferOutboundStatusEnum.three.getValue(), outboundDo.getStatus())) {
            throw new BizException(SysConstant.Transfer_Outbound_Non_To_Be_Shipped_Status);
        }
    }

    @Override
    public RecheckOperateVo recheck(RecheckBo bo, Long userId, String username) {
        RecheckOperateVo recheckOperateVo = new RecheckOperateVo();
        recheckOperateVo.setOutboundId(bo.getId());
        OrderOutboundDo outboundDo = orderOutboundDao.selectById(bo.getId());
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.Scan_OutboundNo_Error);
        }
        //强制通过
        if (bo.isDirectPass()) {
            //直接通过
            outboundProductService.lambdaUpdate()
                    .eq(OrderOutboundProductDo::getOutboundId, bo.getId())
                    .setSql(bo.isBoxOutboundFlag(), "recheck_qty = forecast_box_qty")
                    .setSql(!bo.isBoxOutboundFlag(), "recheck_qty = forecast_qty")
                    .update();
            //完成复核修改出库及波次的状态
            completeRecheckHandler(userId, username, outboundDo, SysConstant.OrderOutbound_Recheck_Direct_Finish);
            recheckOperateVo.setFinish(true);
            return recheckOperateVo;
        }
        //校验状态
        checkRecheckStatus(outboundDo);
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, bo.getId())
                .list();
        int waitRecheckQty = 0;
        if (bo.isBoxOutboundFlag()) {
            //出库箱
            Map<String, List<OrderOutboundProductDo>> boxTypeNoMatchProducts = outboundProductDoList.stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getBoxTypeNumber));
            //判断是否扫描箱号
            boolean boxTypeNoScan = boxTypeNoMatchProducts.containsKey(bo.getScan());
            //判断是否扫描itf条码
            List<String> boxTypeNoList = null;
            if (!boxTypeNoScan) {
                boxTypeNoList = boxService.listObjs(
                        Wrappers.<OrderBoxDo>lambdaQuery()
                                .in(OrderBoxDo::getBoxTypeNumber, boxTypeNoMatchProducts.keySet())
                                .eq(OrderBoxDo::getCustomizeBarcode, bo.getScan())
                                .select(OrderBoxDo::getBoxTypeNumber)
                                .isNotNull(OrderBoxDo::getBoxTypeNumber)
                        , obj -> (String) obj);
            }
            if (!boxTypeNoScan && CollUtil.isEmpty(boxTypeNoList)) {
                throw new BizException(SysConstant.Recheck_Box_Error);
            }
            boolean scanResult = false;
            if (boxTypeNoScan) {
                //箱号扫描
                List<OrderOutboundProductDo> outboundProductsByBoxNo = boxTypeNoMatchProducts.get(bo.getScan());
                for (OrderOutboundProductDo outboundProductDo : outboundProductsByBoxNo) {
                    if (bo.getQty() > outboundProductDo.getOutboundQty() - outboundProductDo.getRecheckQty()) {
                        throw new BizException(SysConstant.Recheck_Qty_Over);
                    }
                    outboundProductDo.setRecheckQty(outboundProductDo.getRecheckQty() + bo.getQty());
                    scanResult = true;
                }
                outboundProductService.updateBatchById(outboundProductsByBoxNo);
            } else {
                //itf扫描
                AtomicInteger qty = new AtomicInteger(bo.getQty());
                List<OrderOutboundProductDo> outboundProductByUpdate = new ArrayList<>();
                for (Map.Entry<String, List<OrderOutboundProductDo>> entry : boxTypeNoMatchProducts.entrySet()) {
                    String boxTypeNumber = entry.getKey();
                    List<OrderOutboundProductDo> value = entry.getValue();
                    if (boxTypeNoList.contains(boxTypeNumber)) {
                        OrderOutboundProductDo outboundProductDo = value.get(0);
                        if (qty.get() <= 0) {
                            break;
                        }
                        int quantity = outboundProductDo.getOutboundBoxQty() - outboundProductDo.getRecheckQty();
                        if (quantity == 0) {
                            continue;
                        }
                        if (qty.get() >= quantity) {
                            value.forEach(item -> item.setRecheckQty(item.getOutboundBoxQty()));
                            outboundProductByUpdate.addAll(value);
                            qty.set(qty.get() - quantity);
                        } else {
                            value.forEach(item -> item.setRecheckQty(item.getRecheckQty() + qty.get()));
                            outboundProductByUpdate.addAll(value);
                            qty.set(0);
                        }
                    }
                }
                if (qty.get() > 0) {
                    throw new BizException(SysConstant.Recheck_Qty_Over);
                }
                outboundProductService.updateBatchById(outboundProductByUpdate);
                scanResult = true;
            }
            if (!scanResult) {
                throw new BizException(SysConstant.Recheck_Box_Error);
            }
            //判断是否复核完成
            waitRecheckQty = outboundProductDoList.stream().map(item -> item.getOutboundBoxQty() - item.getRecheckQty()).reduce(0, Integer::sum);
        } else {
            //出库产品
            List<Long> pIdList = productService.listObjs(Wrappers.<ProductDo>lambdaQuery()
                            .eq(ProductDo::getSku, bo.getScan())
                            .or()
                            .eq(ProductDo::getJanCode, bo.getScan())
                            .select(ProductDo::getId)
                    , obj -> (Long) obj);
            if (CollUtil.isEmpty(pIdList)) {
                throw new BizException(SysConstant.Recheck_Product_Error);
            }
            boolean scanResult = false;
            for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                if (pIdList.contains(outboundProductDo.getProductId())
                    && outboundProductDo.getRecheckQty() < outboundProductDo.getOutboundQty()) {
                    if (bo.getQty() > outboundProductDo.getOutboundQty() - outboundProductDo.getRecheckQty()) {
                        throw new BizException(SysConstant.Recheck_Qty_Over);
                    }
                    scanResult = true;
                    outboundProductDo.setRecheckQty(outboundProductDo.getRecheckQty() + bo.getQty());
                    outboundProductService.updateById(outboundProductDo);
                    break;
                }
            }
            if (!scanResult) {
                throw new BizException(SysConstant.Recheck_Product_Error);
            }
            //判断是否复核完成
            waitRecheckQty = outboundProductDoList.stream().map(item -> item.getOutboundQty() - item.getRecheckQty()).reduce(0, Integer::sum);
        }
        if (waitRecheckQty == 0) {
            //完成复核修改出库及波次的状态
            completeRecheckHandler(userId, username, outboundDo, SysConstant.OrderOutbound_Recheck_Finish);
            recheckOperateVo.setFinish(true);
        }
        return recheckOperateVo;
    }

    public void completeRecheckHandler(Long userId, String username, OrderOutboundDo outboundDo, String operateKey) {
        //修改进度、日志、状态
        outboundService.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundDo.getId())
                .set(Objects.equals(OutboundTypeEnum.one.getValue(), outboundDo.getOrderType()), OrderOutboundDo::getStatus, NormalOutboundStatusEnum.four.getValue())
                .set(OrderOutboundDo::getRecheckTime, DateUtil.date())
                .update();
        operateRecordService.operateAddRecordAndValue(
                operateKey,
                "",
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        //判断所属波次是否全部复核完成
        if (Objects.equals(OutboundTypeEnum.one.getValue(), outboundDo.getOrderType())) {
            //进度
            progressService.lambdaUpdate()
                    .eq(ProgressDo::getBizNumber, outboundDo.getId())
                    .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                    .eq(ProgressDo::getBizType, "复核")
                    .set(ProgressDo::getBizType, "复核完成")
                    .set(ProgressDo::getOperateBy, username)
                    .set(ProgressDo::getOperateTime, DateUtil.date())
                    .update();
            Long waveId = outboundDo.getWaveId();
            boolean allMatch = orderOutboundDao.selectList(
                            Wrappers.<OrderOutboundDo>lambdaQuery().eq(OrderOutboundDo::getWaveId, waveId))
                    .stream().allMatch(item -> NormalOutboundStatusEnum.four.getValue().equals(item.getStatus()));
            if (allMatch) {
                this.lambdaUpdate()
                        .eq(OutboundWaveDo::getId, waveId)
                        .set(OutboundWaveDo::getRecheck, Boolean.TRUE)
                        .set(OutboundWaveDo::getRecheckTime, DateUtil.date())
                        .update();
                operateRecordService.operateAddRecordAndValue(
                        SysConstant.Wave_Recheck_Complete,
                        "",
                        waveId,
                        userId,
                        username,
                        OperateTypeEnum.Wave
                );
            }
        }
    }

    @Override
    public void recheckClearRedo(RecheckClearRedoBo bo, Long userId, String username) {
        OrderOutboundDo outboundDo = orderOutboundDao.selectById(bo.getId());
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.Scan_OutboundNo_Error);
        }
        checkRecheckStatus(outboundDo);
        outboundProductService.lambdaUpdate()
                .eq(OrderOutboundProductDo::getOutboundId, bo.getId())
                .set(OrderOutboundProductDo::getRecheckQty, 0)
                .update();
    }

    @Override
    public void printWavePickingListByClassify(Long id, HttpServletResponse response) throws IOException {
        if (Objects.isNull(id)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        OutboundWaveDo waveDo = this.getById(id);
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        response.setHeader("Content-Type", "application/pdf");
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setHeader("Content-Disposition", "attachment;filename=printWavePickingListByClassify.pdf");
        byte[] waveNoBarcode = BarCodeUtils.generateBarCode128(
                waveDo.getWaveNo(),
                true,
                false,
                256,
                15,
                0.3f,
                0f,
                0f
        );
        ByteArrayInputStream waveNoBarcodeImg = new ByteArrayInputStream(waveNoBarcode);
        String picker = "-";
        if (Objects.nonNull(waveDo.getPicker())) {
            UserDo userDo = userService.getById(waveDo.getPicker());
            picker = userDo.getUserName();
        }
        //通用参数
        Map<String, Object> data = new HashMap<>(16);
        data.put("waveNo", waveDo.getWaveNo());
        data.put("orderNum", waveDo.getOrderNum());
        data.put("printTime", DateUtil.date());
        data.put("picker", picker);
        data.put("waveNoBarcode", Pictures.ofStream(waveNoBarcodeImg, PictureType.PNG).size(225, 60).create());
        //查询波次订单
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery()
                        .eq(OrderOutboundDo::getWaveId, waveDo.getId())
        );
        //查询波次产品
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .list();
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //查询拣货信息
        List<OutboundPickingDo> pickingDoList = pickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                .list();
        //查询库位信息
        Map<Long, StorageLocationDo> rackIdMatch = storageLocationService.lambdaQuery()
                .in(StorageLocationDo::getId, pickingDoList.stream().map(OutboundPickingDo::getRackId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(StorageLocationDo::getId, val -> val));
        data.put("skuKind", outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).distinct().count());
        data.put(
                "skuTotalQty",
                outboundProductDoList.stream()
                        .map(OrderOutboundProductDo::getForecastQty)
                        .reduce(0, Integer::sum)
        );
        ConfigureBuilder configureBuilder = Configure.builder();
        List<Map<String, Object>> tableList = new ArrayList<>();
        Map<String, Object> tableSkuList = new HashMap<>();
        AtomicInteger i = new AtomicInteger(1);
        for (OrderOutboundDo outboundDo : outboundDoList) {
            byte[] outboundNoBarcode = BarCodeUtils.generateBarCode128(
                    outboundDo.getOutboundNo(),
                    true,
                    false,
                    256,
                    15,
                    0.3f,
                    0f,
                    0f
            );
            ByteArrayInputStream outboundNoBarcodeImg = new ByteArrayInputStream(outboundNoBarcode);
            List<WavePickListByClassifyTableDto> dtoList = new ArrayList<>();
            //按sku分组
            Map<Long, List<OrderOutboundProductDo>> outProductListBySkuGroup = outboundProductDoList.stream()
                    .filter(item -> item.getOutboundId().equals(outboundDo.getId()))
                    .collect(Collectors.groupingBy(OrderOutboundProductDo::getProductId));
            for (Map.Entry<Long, List<OrderOutboundProductDo>> entry : outProductListBySkuGroup.entrySet()) {
                ProductDo productDo = productIdMatch.get(entry.getKey());
                WavePickListByClassifyTableDto dto = new WavePickListByClassifyTableDto();
                dto.setSku(productDo.getSku());
                dto.setProductName(productDo.getProductName());
                //查询拣货信息
                List<Long> opIdListBySku = entry.getValue().stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList());
                List<OutboundPickingDo> pickingDoListBySku = pickingDoList.stream()
                        .filter(item -> opIdListBySku.contains(item.getOutboundProductId()))
                        .collect(Collectors.toList());
                //合并相同库位相同状态的数据
                pickingDoListBySku.stream()
                        .collect(Collectors.toMap(k -> k.getRackId() + ":" + k.getPickFlag(), val -> val, (o1, o2) -> {
                            o1.setQty(o1.getQty() + o2.getQty());
                            return o1;
                        })).values().forEach(pickingDo -> {
                            HashMap<String, String> map = new HashMap<>(8);
                            map.put("cellCode", rackIdMatch.get(pickingDo.getRackId()).getCode());
                            map.put("qty", String.valueOf(pickingDo.getQty()));
                            map.put("state", pickingDo.getPickFlag() ? "Done" : "Not done");
                            dto.getSonList().add(map);
                        });
                dtoList.add(dto);
            }
            Map<String, Object> table = new HashMap<>(8);
            table.put("outboundNo", outboundDo.getOutboundNo());
            table.put("outboundNoBarcode", Pictures.ofStream(outboundNoBarcodeImg, PictureType.PNG).size(240, 60).create());
            table.put("trackingNumber", StrUtil.isEmpty(outboundDo.getTrackingNumber()) ? "-" : outboundDo.getTrackingNumber());
            table.put("carrier", outboundDo.getCarrier());
            if (CollUtil.isNotEmpty(dtoList) && CollUtil.isNotEmpty(dtoList.stream().map(WavePickListByClassifyTableDto::getSonList).flatMap(List::stream).collect(Collectors.toList()))) {
                tableSkuList.put(
                        "table" + i.get(),
                        this.getWavePickListByClassifyTableData(
                                dtoList,
                                new DynamicTableDto(0, 0, 2, 5, 4)
                        )
                );
                configureBuilder.bind("table" + i.getAndIncrement(), new DynamicTablePolicy());
            } else {
                tableSkuList.put("table" + i.getAndIncrement(), "（订单已取消）");
            }
            tableList.add(table);
        }
        data.put("tableList", tableList);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
        ServletOutputStream out = response.getOutputStream();
        //模板配置
        XWPFTemplate template = XWPFTemplate.compile(this.getClass().getClassLoader().getResourceAsStream("wordTemplates/wavePickListByClassify.docx"), Configure.builder().useSpringEL().build()).render(data);
        template.writeAndClose(bos);
        XWPFTemplate template2 = XWPFTemplate.compile(new ByteArrayInputStream(bos.toByteArray()), configureBuilder.build()).render(tableSkuList);
        template2.writeAndClose(bos2);
        //转换为pdf
        ByteArrayOutputStream pdf = AsposePdfUtils.word2pdf(new ByteArrayInputStream(bos2.toByteArray()));
        out.write(pdf.toByteArray());
        bos.flush();
        bos2.flush();
        out.flush();
        PoitlIOUtils.closeQuietlyMulti(template, bos, bos2, out, pdf);
    }

    private DynamicTableDto getWavePickListByClassifyTableData(List<WavePickListByClassifyTableDto> dtoList, DynamicTableDto tableDto) {
        for (WavePickListByClassifyTableDto dto : dtoList) {
            tableDto.getGroupMap().put(dto.getSku(), dto.getSonList().size());
            for (Map<String, String> sonMap : dto.getSonList()) {
                RowRenderData rowRenderData = Rows.of(
                        Cells.of(dto.getSku()).create(),
                        Cells.of(dto.getProductName()).create(),
                        Cells.of(sonMap.get("cellCode")).create(),
                        Cells.of(sonMap.get("qty")).create(),
                        Cells.of(sonMap.get("state")).create()
                ).center().create();
                tableDto.getDataList().add(rowRenderData);
            }
        }
        return tableDto;
    }

    @Override
    public void printWavePickingListByTotal(Long id, HttpServletResponse response) throws IOException {
        if (Objects.isNull(id)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        OutboundWaveDo waveDo = this.getById(id);
        if (Objects.isNull(waveDo)) {
            throw new BizException(SysConstant.WAVE_NOT_EXIST);
        }
        response.setHeader("Content-Type", "application/pdf");
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setHeader("Content-Disposition", "attachment;filename=printWavePickingListByTotal.pdf");
        byte[] waveNoBarcode = BarCodeUtils.generateBarCode128(
                waveDo.getWaveNo(),
                true,
                false,
                256,
                15,
                0.3f,
                0f,
                0f
        );
        ByteArrayInputStream waveNoBarcodeImg = new ByteArrayInputStream(waveNoBarcode);
        String picker = "-";
        if (Objects.nonNull(waveDo.getPicker())) {
            UserDo userDo = userService.getById(waveDo.getPicker());
            picker = userDo.getUserName();
        }
        //通用参数
        Map<String, Object> data = new HashMap<>(16);
        data.put("waveNo", waveDo.getWaveNo());
        data.put("orderNum", waveDo.getOrderNum());
        data.put("printTime", DateUtil.date());
        data.put("picker", picker);
        data.put("waveNoBarcode", Pictures.ofStream(waveNoBarcodeImg, PictureType.PNG).size(225, 60).create());
        //查询波次订单
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery()
                        .eq(OrderOutboundDo::getWaveId, waveDo.getId())
        );
        //查询波次产品
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .list();
        //按sku分组
        Map<Long, List<OrderOutboundProductDo>> outProductListBySkuGroup = outboundProductDoList.stream()
                .collect(Collectors.groupingBy(OrderOutboundProductDo::getProductId));
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outProductListBySkuGroup.keySet())
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //查询拣货信息
        List<OutboundPickingDo> pickingDoList = pickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                .list();
        //查询库位信息
        Map<Long, StorageLocationDo> rackIdMatch = storageLocationService.lambdaQuery()
                .in(StorageLocationDo::getId, pickingDoList.stream().map(OutboundPickingDo::getRackId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(StorageLocationDo::getId, val -> val));
        List<WavePickListByTotalTableDto> dtoList = new ArrayList<>();
        for (Map.Entry<Long, List<OrderOutboundProductDo>> entry : outProductListBySkuGroup.entrySet()) {
            ProductDo productDo = productIdMatch.get(entry.getKey());
            WavePickListByTotalTableDto dto = new WavePickListByTotalTableDto();
            dto.setSku(productDo.getSku());
            dto.setJanCode(productDo.getJanCode());
            dto.setProductName(productDo.getProductName());
            List<Long> opIdListBySku = entry.getValue().stream()
                    .map(OrderOutboundProductDo::getId)
                    .collect(Collectors.toList());
            List<OutboundPickingDo> pickingDoListBySku = pickingDoList.stream()
                    .filter(item -> opIdListBySku.contains(item.getOutboundProductId()))
                    .collect(Collectors.toList());
            //合并相同库位相同状态的数据
            pickingDoListBySku.stream()
                    .collect(Collectors.toMap(k -> k.getRackId() + ":" + k.getPickFlag(), val -> val, (o1, o2) -> {
                        o1.setQty(o1.getQty() + o2.getQty());
                        return o1;
                    })).values().forEach(pickingDo -> {
                        HashMap<String, String> map = new HashMap<>(8);
                        map.put("cellCode", rackIdMatch.get(pickingDo.getRackId()).getCode());
                        map.put("qty", String.valueOf(pickingDo.getQty()));
                        map.put("state", pickingDo.getPickFlag() ? "Done" : "Not done");
                        dto.getSonList().add(map);
                    });
            dtoList.add(dto);
        }
        data.put(
                "table",
                this.getWavePickListByTotalTableData(
                        dtoList,
                        new DynamicTableDto(0, 0, 3, 6, 1)
                )
        );
        data.put("skuKind", outProductListBySkuGroup.keySet().size());
        data.put(
                "skuTotalQty",
                outProductListBySkuGroup.values().stream()
                        .flatMap(Collection::stream)
                        .map(OrderOutboundProductDo::getForecastQty)
                        .reduce(0, Integer::sum)
        );
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ServletOutputStream out = response.getOutputStream();
        //模板配置
        Configure configure = Configure.builder().bind("table", new DynamicTablePolicy()).build();
        XWPFTemplate template = XWPFTemplate.compile(this.getClass().getClassLoader().getResourceAsStream("wordTemplates/wavePickListByTotal.docx"), configure).render(data);
        template.writeAndClose(bos);
        //转换为pdf
        ByteArrayOutputStream pdf = AsposePdfUtils.word2pdf(new ByteArrayInputStream(bos.toByteArray()));
        out.write(pdf.toByteArray());
        bos.flush();
        out.flush();
        PoitlIOUtils.closeQuietlyMulti(template, bos, out, pdf);
    }

    @Override
    public void waveForceFinish(Long waveId, Long userId, String username) {
        if (Objects.isNull(waveId)) {
            return;
        }
        OutboundWaveDo waveDo = this.getById(waveId);
        if (Objects.isNull(waveDo)) {
            return;
        }
        if (!Objects.equals(waveDo.getStatus(), WaveStatusEnum.one.getValue())) {
            throw new BizException(SysConstant.Status_Error);
        }
        List<OrderOutboundDo> outboundDoList = orderOutboundDao.selectList(
                Wrappers.<OrderOutboundDo>lambdaQuery()
                        .eq(OrderOutboundDo::getWaveId, waveId)
        );
        if (CollUtil.isEmpty(outboundDoList)) {
            return;
        }
        wavePickingFinishHandler(waveId, userId, username, SysConstant.Wave_Force_Pick_Finish, outboundDoList);
        //处理多拣的情况 少拣的情况目前不存在
        List<Long> outIdList = outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList());
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outIdList)
                .list();
        List<Long> outProductIdList = outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList());
        outboundProductService.lambdaUpdate()
                .in(OrderOutboundProductDo::getOutboundId, outIdList)
                .setSql("outbound_qty = forecast_qty")
                .update();
        pickingService.lambdaUpdate()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outProductIdList)
                .set(OutboundPickingDo::getPickFlag, Boolean.TRUE)
                .update();
        Map<Long, List<OutboundPickingDo>> outProductIdMatchPickList = pickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outProductIdList)
                .list().stream().collect(Collectors.groupingBy(OutboundPickingDo::getOutboundProductId));
        for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
            List<OutboundPickingDo> pickingDoList = outProductIdMatchPickList.get(outboundProductDo.getId());
            if (CollUtil.isEmpty(pickingDoList)) {
                continue;
            }
            int pickingQty = pickingDoList.stream().mapToInt(OutboundPickingDo::getQty).reduce(0, Integer::sum);
            Integer forecastQty = outboundProductDo.getForecastQty();
            if (pickingQty > forecastQty) {
                outboundService.unLock(
                        JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                        pickingDoList,
                        new AtomicInteger(pickingQty - forecastQty)
                );
            }
        }
        this.lambdaUpdate()
                .eq(OutboundWaveDo::getId, waveId)
                .setSql("picking_qty = should_picking_qty")
                .update();
    }

    private DynamicTableDto getWavePickListByTotalTableData(List<WavePickListByTotalTableDto> dtoList, DynamicTableDto tableDto) {
        for (WavePickListByTotalTableDto dto : dtoList) {
            tableDto.getGroupMap().put(dto.getSku(), dto.getSonList().size());
            ByteArrayInputStream barcodeImg = null;
            if (StrUtil.isNotBlank(dto.getJanCode())) {
                //入库单号条形
                byte[] barcode = BarCodeUtils.generateBarCode128(
                        dto.getJanCode(),
                        false,
                        false,
                        256,
                        15,
                        0.3f,
                        0,
                        0
                );
                if (barcode != null) {
                    barcodeImg = new ByteArrayInputStream(barcode);
                }
            }
            for (Map<String, String> sonMap : dto.getSonList()) {
                RowRenderData rowRenderData = Rows.of(
                        Cells.of(dto.getSku()).create(),
                        Objects.isNull(barcodeImg)
                                ? Cells.of("-").create()
                                : Cells.of(Pictures.ofStream(barcodeImg, PictureType.PNG).size(190, 55).create()).create(),
                        Cells.of(dto.getProductName()).create(),
                        Cells.of(sonMap.get("cellCode")).create(),
                        Cells.of(sonMap.get("qty")).create(),
                        Cells.of(sonMap.get("state")).create()
                ).center().create();
                tableDto.getDataList().add(rowRenderData);
            }
        }
        return tableDto;
    }

    /**
     * 锁/解库存并保存拣货记录
     *
     * @param productStockRackDoList 可以锁定的库存数据
     * @param needUnlock             需要解锁的数量
     * @param sku                    sku
     * @param unLckPickingList       可供锁定的拣货记录
     * @param noPickingDoList        没有拣货的记录
     */
    public void lockAndUnlockStockAndSavePicking(List<ProductStockRackDo> productStockRackDoList,
                                                 AtomicInteger needUnlock,
                                                 String sku,
                                                 List<OutboundPickingDo> unLckPickingList,
                                                 List<OutboundPickingDo> noPickingDoList) throws BizException {
        unLckPickingList.stream()
                .sorted(Comparator.comparing(OutboundPickingDo::getSubStockId))
                .forEach(item -> {
                    if (needUnlock.get() == 0) {
                        //跳过档次循环
                        return;
                    }
                    if (item.getQty() <= needUnlock.get()) {
                        productStockRackService.lambdaUpdate()
                                .eq(ProductStockRackDo::getId, item.getSubStockId())
                                .setSql("quantity = quantity + " + item.getQty())
                                .setSql("lock_quantity = lock_quantity - " + item.getQty())
                                .update();
                        pickingService.removeById(item.getId());
                        noPickingDoList.forEach(noPickingDo -> {
                            if (noPickingDo.getId().equals(item.getId())) {
                                noPickingDo.setPickFlag(null);
                            }
                        });
                        this.lockStockAndSavePicking(
                                productStockRackDoList,
                                new AtomicInteger(item.getQty()),
                                sku,
                                item.getOutboundProductId(),
                                Boolean.TRUE
                        );
                        needUnlock.set(needUnlock.get() - item.getQty());
                    } else {
                        productStockRackService.lambdaUpdate()
                                .eq(ProductStockRackDo::getId, item.getSubStockId())
                                .setSql("quantity = quantity + " + needUnlock.get())
                                .setSql("lock_quantity = lock_quantity - " + item.getQty())
                                .update();
                        pickingService.lambdaUpdate()
                                .eq(OutboundPickingDo::getId, item.getId())
                                .setSql("qty = qty -" + needUnlock.get())
                                .update();
                        //修改待拣货列表数据
                        noPickingDoList.forEach(noPickingDo -> {
                            if (noPickingDo.getId().equals(item.getId())) {
                                noPickingDo.setQty(noPickingDo.getQty() - needUnlock.get());
                            }
                        });
                        this.lockStockAndSavePicking(
                                productStockRackDoList,
                                needUnlock,
                                sku,
                                item.getOutboundProductId(),
                                Boolean.TRUE
                        );
                        needUnlock.set(0);
                    }
                });
    }

    /**
     * 锁库存并保存拣货记录
     *
     * @param productStockRackDoList 可以锁定的库存数据
     * @param needLock               需要锁定的数量
     * @param sku                    sku
     * @param outboundProductId      出库单产品表id
     */
    public void lockStockAndSavePicking(
            List<ProductStockRackDo> productStockRackDoList,
            AtomicInteger needLock,
            String sku,
            Long outboundProductId,
            Boolean pickFlag) throws BizException {
        if (CollUtil.isEmpty(productStockRackDoList)) {
            throw new BizException(SysConstant.Inventory_Shortage, sku);
        }
        List<OutboundPickingDo> outboundPickingDoList = new ArrayList<>();
        for (ProductStockRackDo productStockRackDo : productStockRackDoList.stream().sorted(Comparator.comparing(ProductStockRackDo::getId)).collect(Collectors.toList())) {
            if (needLock.get() <= 0) {
                break;
            }
            //该条记录可用数量
            Integer quantity = productStockRackDo.getQuantity();
            if (quantity == 0) {
                continue;
            }
            int qty;
            if (quantity <= needLock.get()) {
                qty = quantity;
                needLock.set(needLock.get() - quantity);
            } else {
                qty = needLock.get();
                needLock.set(0);
            }
            productStockRackDo.setQuantity(quantity - qty);
            //锁定库存
            productStockRackService.lambdaUpdate()
                    .eq(ProductStockRackDo::getId, productStockRackDo.getId())
                    .set(ProductStockRackDo::getQuantity, quantity - qty)
                    .set(ProductStockRackDo::getLockQuantity, productStockRackDo.getLockQuantity() + qty)
                    .update();
            OutboundPickingDo outboundPickingDo = new OutboundPickingDo();
            outboundPickingDo.setOrderType(OutboundOrderKind.outbound.getValue());
            outboundPickingDo.setOutboundProductId(outboundProductId);
            outboundPickingDo.setSubStockId(productStockRackDo.getId());
            outboundPickingDo.setRackId(productStockRackDo.getRackId());
            outboundPickingDo.setQty(qty);
            outboundPickingDo.setPickFlag(pickFlag);
            outboundPickingDoList.add(outboundPickingDo);
        }
        if (needLock.get() > 0) {
            throw new BizException(SysConstant.Inventory_Shortage, sku);
        }
        //批量保存系统默认拣货记录
        pickingService.saveBatch(outboundPickingDoList);
    }
}
